package com.gzz.orchid.common.boot.asynclock;

import com.gzz.orchid.common.core.util.CryptoUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.data.redis.core.ConvertingCursor;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.scripting.support.ResourceScriptSource;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 分布式锁
 *
 * @author higmd
 */
@Slf4j
public class DistributedLockHepler {
    //前缀
    private final static String KEY_PREFIX = "async-lock";
    //过期时间
    private final static int EXPIRE_TIME = 5000;
    @Lazy
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 格式化锁的键
     */
    private String formatLockKey(String key) {
        return String.format("%s:%s", KEY_PREFIX, key);
    }

    /**
     * 获取锁
     */
    public boolean lock(String key, String lockId) {
        //// Lua脚本用于在Redis中执行原子操作，确保在加锁时不会被其他操作干扰
        //String luaScript = "if redis.call('set', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) then return 1 else return 0 end";
        //// 创建RedisScript对象，指定Lua脚本和返回值类型
        //RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
        //// 执行Lua脚本，参数包括key、val和expire，分别对应锁的键、值和过期时间
        //Long state = stringRedisTemplate.execute(redisScript, Collections.singletonList(key), val, expire);
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis-scripts/try-lock.lua")));
        redisScript.setResultType(Long.class);
        //
        Long retval = stringRedisTemplate.execute(redisScript
                , List.of(formatLockKey(key))
                , lockId
                , EXPIRE_TIME);
        log.info("获取锁 -{} -【{}】", retval, retval == 1 ? "成功" : "失败");
        return retval.intValue() == 1;
    }

    /**
     * 批量获取锁(必须同时能锁住)
     */
    public boolean tryBatchLock(Object... args) throws IOException {
        Resource resource = new ClassPathResource("redis-scripts/batch-lock.lua");
        String luaScript = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
        // RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
        RedisScript<Long> redisScript = RedisScript.of(luaScript, Long.class);
        // 执行Lua脚本，参数包括key、val和expire，分别对应锁的键、值和过期时间
        long start = System.currentTimeMillis();
        try {
            while ((System.currentTimeMillis() - start) < 3000) {
                Long resut = stringRedisTemplate.execute(redisScript,
                        List.of(KEY_PREFIX + ":", UUID.randomUUID().toString()),
                        args);
                if (resut == 1) {
                    return Boolean.TRUE;
                } else {
                    // 获取锁失败，等待一段时间(100毫秒)再尝试获取
                    TimeUnit.MILLISECONDS.sleep(100);
                }
            }
        } catch (Exception e) {
            log.error("分布式tryLock error:{}", e.getMessage());
        }
        return Boolean.FALSE;
    }

    /**
     * 使用Lua脚本实现获取分布式锁
     *
     * @param key 分布式锁的键，通常与业务场景相关
     * @param lockId 锁的值，用于确保只有锁的持有者才能释放锁，通常为一个唯一标识符（如UUID）
     */
    public boolean tryGetLock(String key, String lockId) {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis-scripts/try-lock.lua")));
        redisScript.setResultType(Long.class);

        long start = System.currentTimeMillis();
        try {
            // 循环尝试获取锁，多尝试3秒
            while ((System.currentTimeMillis() - start) < 3000) {
                //boolean resut = stringRedisTemplate.opsForValue().setIfAbsent(formatLockKey(key), lockId, 10, TimeUnit.SECONDS)) {
                // 执行Lua脚本，过期时间（毫秒）
                Long resut = stringRedisTemplate.execute(redisScript
                        , Collections.singletonList(formatLockKey(key))
                        , lockId, EXPIRE_TIME);
                if (resut.intValue() == 1) {
                    // 获取锁成功
                    return true;
                } else {
                    // 获取锁失败，等待一段时间再尝试获取
                    TimeUnit.MILLISECONDS.sleep(100);
                }
            }
        } catch (Exception e) {
            log.error("分布式tryLock error: {}", e.getMessage());
        }
        return Boolean.FALSE;
    }

    public boolean tryGetLock(Object key) {
        return tryGetLock(CryptoUtil.md5Encode(String.valueOf(key)), UUID.randomUUID().toString());
    }

    /**
     * 使用Redis进行分布式锁的释放操作
     *
     * @param key    分布式锁的键，通常与业务场景相关
     * @param lockId 锁的值，用于确保只有锁的持有者才能释放锁，通常为一个唯一标识符（如UUID）
     */
    public void releaseLock(String key, String lockId) {
        try {
            // 使用Lua脚本执行原子操作，确保只有锁的持有者才能删除锁
            // 这里使用了if条件判断，如果锁的值与传入的值匹配，则删除锁；否则，返回0
            String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            // 创建一个RedisScript对象，指定Lua脚本和返回类型
            RedisScript<Boolean> redisScript = new DefaultRedisScript<>(luaScript, Boolean.class);
            // 执行Lua脚本，传入键和值作为参数
            boolean releaseLock = stringRedisTemplate.execute(redisScript, Collections.singletonList(formatLockKey(key)), lockId);
            //boolean releaseLock = Boolean.TRUE.equals(returnValue);
            // 方法二：使用Lua脚本执行原子操作，确保只有锁的持有者才能删除锁
            //Resource resource = new ClassPathResource("releaseLock.lua");
            //String luaScript = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
            //DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(luaScript, Boolean.class);
            ////script.setScriptSource(new ResourceScriptSource(new ClassPathResource("releaseLock.lua")));
            //Boolean releaseLock = stringRedisTemplate.execute(redisScript, List.of(formatLockKey(key)));
            log.info("释放锁 -{} -【{}】", releaseLock, releaseLock ? "成功" : "失败");
        } catch (Exception e) {
            // 异常处理块，目前未实现具体逻辑
            log.error("释放锁 -{}", e.getMessage());
        }
    }

    /**
     * 释放锁(粗爆的方法)
     */
    public void unlock(Object... keys) {
        List<String> keyList = Arrays.stream(keys)
                .map(key -> formatLockKey(CryptoUtil.md5Encode(String.valueOf(key))))
                .collect(Collectors.toList());
        //
        stringRedisTemplate.delete(keyList);
    }

    /**
     * 释放锁
     */
    public void releaseLock(Object key) {
        stringRedisTemplate.delete(formatLockKey(CryptoUtil.md5Encode(String.valueOf(key))));
    }

    /**
     * cursor
     * 自定义 redis scan 操作
     */
    public Cursor<String> scan(String pattern, Long limit) {
        ScanOptions options = ScanOptions.scanOptions().match(pattern).count(limit).build();
        // 获取RedisSerializer对象，用于反序列化字节数组为字符串
        // RedisSerializer<String> serializer = (RedisSerializer<String>) stringRedisTemplate.getKeySerializer();
        // return stringRedisTemplate
        //        .executeWithStickyConnection(conn -> new ConvertingCursor<>(conn.scan(options), serializer::deserialize));
        return stringRedisTemplate.executeWithStickyConnection(redisConnection -> {
            Cursor<byte[]> cursor = redisConnection.scan(options);
            return new ConvertingCursor<>(cursor, bytes -> new String(bytes, StandardCharsets.UTF_8));
        });
    }
}
